Best trading strategies that rely on technical analysis might take into account price action on multiple time frames. This tutorial will show how to do that with backtesting.py, offloading most of the work to pandas resampling. It is assumed you're already familiar with basic framework usage.
We will put to the test this long-only, supposed 400%-a-year trading strategy, which uses daily and weekly relative strength index (RSI) values and moving averages (MA).
In practice, one should use functions from an indicator library, such as TA-Lib or Tulipy, but among us, let's introduce the two indicators we'll be using.
import pandas as pd
def SMA(array, n):
"""Simple moving average"""
return pd.Series(array).rolling(n).mean()
def RSI(array, n):
"""Relative strength index"""
# Approximate; good enough
gain = pd.Series(array).diff()
loss = gain.copy()
gain[gain < 0] = 0
loss[loss > 0] = 0
rs = gain.ewm(n).mean() / loss.abs().ewm(n).mean()
return 100 - 100 / (1 + rs)
The strategy roughly goes like this:
Buy a position when:
Close the position when:
We need to provide bars data in the lowest time frame (i.e. daily) and resample it to any higher time frame (i.e. weekly) that our strategy requires.
from backtesting import Strategy, Backtest
from backtesting.lib import resample_apply
class System(Strategy):
d_rsi = 30 # Daily RSI lookback periods
w_rsi = 30 # Weekly
level = 70
def init(self):
# Compute moving averages the strategy demands
self.ma10 = self.I(SMA, self.data.Close, 10)
self.ma20 = self.I(SMA, self.data.Close, 20)
self.ma50 = self.I(SMA, self.data.Close, 50)
self.ma100 = self.I(SMA, self.data.Close, 100)
# Compute daily RSI(30)
self.daily_rsi = self.I(RSI, self.data.Close, self.d_rsi)
# To construct weekly RSI, we can use `resample_apply()`
# helper function from the library
self.weekly_rsi = resample_apply(
'W-FRI', RSI, self.data.Close, self.w_rsi)
def next(self):
price = self.data.Close[-1]
# If we don't already have a position, and
# if all conditions are satisfied, enter long.
if (not self.position and
self.daily_rsi[-1] > self.level and
self.weekly_rsi[-1] > self.level and
self.weekly_rsi[-1] > self.daily_rsi[-1] and
self.ma10[-1] > self.ma20[-1] > self.ma50[-1] > self.ma100[-1] and
price > self.ma10[-1]):
# Buy at market price on next open, but do
# set 8% fixed stop loss.
self.buy(sl=.92 * price)
# If the price closes 2% or more below 10-day MA
# close the position, if any.
elif price < .98 * self.ma10[-1]:
self.position.close()
Let's see how our strategy fares replayed on nine years of Google stock data.
from backtesting.test import GOOG
backtest = Backtest(GOOG, System, commission=.002)
backtest.run()
Start 2004-08-19 00:00:00 End 2013-03-01 00:00:00 Duration 3116 days 00:00:00 Exposure Time [%] 2.79 Equity Final [$] 10017.44 Equity Peak [$] 10978.38 Return [%] 0.17 Buy & Hold Return [%] 703.46 Return (Ann.) [%] 0.02 Volatility (Ann.) [%] 4.94 Sharpe Ratio 0.00 Sortino Ratio 0.01 Calmar Ratio 0.00 Max. Drawdown [%] -10.01 Avg. Drawdown [%] -9.34 Max. Drawdown Duration 2653 days 00:00:00 Avg. Drawdown Duration 1410 days 00:00:00 # Trades 4 Win Rate [%] 25.00 Best Trade [%] 9.69 Worst Trade [%] -4.46 Avg. Trade [%] 0.08 Max. Trade Duration 35 days 00:00:00 Avg. Trade Duration 21 days 00:00:00 Profit Factor 1.11 Expectancy [%] 0.23 SQN 0.01 _strategy System _equity_curve Equ... _trades Size EntryBa... dtype: object
Meager four trades in the span of nine years and with zero return? How about if we optimize the parameters a bit?
%%time
backtest.optimize(d_rsi=range(10, 35, 5),
w_rsi=range(10, 35, 5),
level=range(30, 80, 10))
CPU times: user 108 ms, sys: 12 ms, total: 120 ms Wall time: 6.2 s
Start 2004-08-19 00:00:00 End 2013-03-01 00:00:00 Duration 3116 days 00:00:00 Exposure Time [%] 22.49 Equity Final [$] 22587.83 Equity Peak [$] 23395.59 Return [%] 125.88 Buy & Hold Return [%] 703.46 Return (Ann.) [%] 10.03 Volatility (Ann.) [%] 13.12 Sharpe Ratio 0.76 Sortino Ratio 1.29 Calmar Ratio 0.53 Max. Drawdown [%] -18.92 Avg. Drawdown [%] -3.80 Max. Drawdown Duration 778 days 00:00:00 Avg. Drawdown Duration 97 days 00:00:00 # Trades 23 Win Rate [%] 65.22 Best Trade [%] 25.03 Worst Trade [%] -6.30 Avg. Trade [%] 3.66 Max. Trade Duration 63 days 00:00:00 Avg. Trade Duration 29 days 00:00:00 Profit Factor 4.98 Expectancy [%] 3.92 SQN 2.61 _strategy System(d_rsi=30,... _equity_curve Equ... _trades Size EntryB... dtype: object
backtest.plot()
Better. While the strategy doesn't perform as well as simple buy & hold, it does so with significantly lower exposure (time in market).
In conclusion, to test strategies on multiple time frames, you need to pass in OHLC data in the lowest time frame, then resample it to higher time frames, apply the indicators, then resample back to the lower time frame, filling in the in-betweens.
Which is what the function backtesting.lib.resample_apply()
does for you.
Learn more by exploring further examples or find more framework options in the full API reference.